/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of SimpleEL.
 * 
 * SimpleEL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * SimpleEL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with SimpleEL. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.simpleel.internal.asm;

import static ch.msoftch.asm.Opcodes.*;
import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import ch.msoftch.asm.*;
import ch.msoftch.asm.Type;
import ch.msoftch.asm.tree.*;
import ch.msoftch.internal.*;

/**
 * Factory building {@link IPropertyAccessor}s.<br>
 * Only for internal use!
 * 
 * @author M. Hautle
 */
class ASMAccessorFactory {
    /** Index of the setter value on the stack. */
    private static final int SETTER_VAL = 3;

    /** Enumeration holding the stack indexes for the methods to implement. */
    private static enum AccessType {
        /** Index values for {@link IPropertyAccessor#get(Object, String)} . */
        GETTER(2, 3, 4),

        /** Index values for {@link IPropertyAccessor#set(Object, String, Object)} . */
        SETTER(2, 4, 5);

        /** Index of the name value on the stack. */
        final int nameVar;

        /** Index of the casted bean value on the stack. */
        final int beanVar;

        /** Index of the property name hashcode on the stack. */
        final int hashVar;

        /**
         * Default constructor.
         * 
         * @param name Index of the name value on the stack
         * @param bean Index of the casted bean value on the stack
         * @param hash Index of the property name hashcode on the stack
         */
        private AccessType(int name, int bean, int hash) {
            nameVar = name;
            beanVar = bean;
            hashVar = hash;
        }
    }

    /** Internal name of {@link Object}. */
    private static final String OBJECT = Type.getInternalName(Object.class);

    /** Internal name of {@link IPropertyAccessor}. */
    private static final String INTERFACE = Type.getInternalName(IPropertyAccessor.class);

    /** Array containing the internal name of {@link Exception}. */
    private static final String[] THROW_EXCEPTION = { "java/lang/Exception" };

    /** Cached 'accessors'. */
    private static final ICache<Class, IPropertyAccessor> INSTANCES = CacheFactory.createCache(Class.class, IPropertyAccessor.class);

    /**
     * Returns an {@link IPropertyAccessor} for the given type.<br>
     * If no such object exists, one will be created.<br>
     * If no class definition for such an accessor exists, one will be created.
     * 
     * @param c The type
     * @return A accessor instance for the given type
     * @throws IllegalArgumentException If null or an invalid type was passed
     */
    public static IPropertyAccessor getAccessor(Class<?> c) throws IllegalArgumentException {
        if (c == null || c.isPrimitive() || c.isArray())
            throw new IllegalArgumentException("Only normal classes and interfaces were accepted!");
        // we don't care about concurrency - the cache is just to avoid unnecessary object trashing
        try {
            IPropertyAccessor o = INSTANCES.get(c);
            if (o == null)
                INSTANCES.put(c, o = getAccessorClass(c).newInstance());
            return o;
        } catch (InstantiationException e) {
            throw new TechnicalException("Error while returning an recording instance", e);
        } catch (IllegalAccessException e) {
            throw new TechnicalException("Error while returning an recording instance", e);
        }
    }

    /**
     * Hidden constructor.
     */
    private ASMAccessorFactory() {
    }

    /**
     * Returns a accessor class from the given type.
     * 
     * @param c The desired type
     * @return The corresponding accessor class
     */
    @SuppressWarnings("unchecked")
    private static synchronized Class<? extends IPropertyAccessor> getAccessorClass(Class<?> c) {
        final String name = buildName(c);
        final ASMClassLoader loader = ASMClassLoader.getLoader(c.getClassLoader());
        Class cl = loader.getLoadedClass(name);
        if (cl == null)
            cl = loader.defineClass(name, build(name, c));
        return cl;
    }

    /**
     * Builds the name for the accessor of the given class.
     * 
     * @param c The class
     * @return The name for the accessor
     */
    private static String buildName(Class<?> c) {
        final String name = c.getName();
        if (name.startsWith("java."))
            return "my" + name + "$acc";
        return name.concat("$acc");
    }

    /**
     * Builds the accessor class.
     * 
     * @param name The name of the new class
     * @param c The class for which to build the acceessor
     * @return The bytecode
     */
    @SuppressWarnings("unchecked")
    private static byte[] build(String name, Class<?> c) {
        final boolean interfaze = c.isInterface();
        final ClassNode cn = new ClassNode();
        cn.name = name.replace('.', '/');
        cn.access = ACC_PUBLIC | ACC_FINAL;
        cn.version = V1_5;
        cn.superName = OBJECT;
        cn.interfaces.add(INTERFACE);
        createDefaultConstructor(cn);
        final String type = Type.getInternalName(c);
        final List<HashEntry> getters = new ArrayList<HashEntry>();
        final List<HashEntry> setters = new ArrayList<HashEntry>();
        buildPropertyLists(c, getters, setters);
        cn.methods.add(buildGetters(type, getters, interfaze));
        cn.methods.add(buildSetters(type, setters, interfaze));
        final ClassWriter w = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cn.accept(w);
        return w.toByteArray();
    }

    /**
     * Add a default constructor.
     * 
     * @param cn The classnode
     */
    @SuppressWarnings("unchecked")
    private static void createDefaultConstructor(ClassNode cn) {
        final MethodNode c = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        final InsnList inst = c.instructions;
        inst.add(new VarInsnNode(ALOAD, 0));
        inst.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, "<init>", "()V"));
        inst.add(new InsnNode(RETURN));
        cn.methods.add(c);
    }

    /**
     * Builds the {@link IPropertyAccessor#get(Object, String)} method.
     * 
     * @param type The internal name of the class to access
     * @param getters The list of the getters
     * @param interfaze True if this accessor is for a interface
     * @return The method
     */
    private static MethodNode buildGetters(String type, final List<HashEntry> getters, boolean interfaze) {
        final MethodNode m = new MethodNode(ACC_PUBLIC | ACC_FINAL, "get", "(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;", null, THROW_EXCEPTION);
        final InsnList inst = m.instructions;
        // binary trees with no elements were hard to build....
        if (getters.size() > 0)
            build(inst, type, getters, AccessType.GETTER, interfaze);
        inst.add(new FieldInsnNode(GETSTATIC, INTERFACE, "UNDEFINED", "Ljava/lang/Object;"));
        inst.add(new InsnNode(ARETURN));
        return m;
    }

    /**
     * Builds the {@link IPropertyAccessor#set(Object, String, Object)} method.
     * 
     * @param type The internal name of the class to access
     * @param setters The list of the setters
     * @param interfaze True if this accessor is for a interface
     * @return The method
     */
    private static MethodNode buildSetters(String type, final List<HashEntry> setters, boolean interfaze) {
        final MethodNode m = new MethodNode(ACC_PUBLIC | ACC_FINAL, "set", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;)V", null, THROW_EXCEPTION);
        final InsnList inst = m.instructions;
        // binary trees with no elements were hard to build....
        if (setters.size() > 0)
            build(inst, type, setters, AccessType.SETTER, interfaze);
        inst.add(new InsnNode(RETURN));
        return m;
    }

    /**
     * Builds the main part of the {@link IPropertyAccessor#get(Object, String)}/{@link IPropertyAccessor#set(Object, String, Object)} method.
     * 
     * @param inst The instruction list of the method
     * @param type The internal name of the type for which the accessor is built
     * @param properties The properties of the type
     * @param methodType The type of the method to build
     * @param interfaze True if this accessor is for a interface
     */
    private static void build(InsnList inst, String type, List<HashEntry> properties, AccessType methodType, boolean interfaze) {
        // cast to expected type
        inst.add(new VarInsnNode(ALOAD, 1));
        inst.add(new TypeInsnNode(CHECKCAST, type));
        inst.add(new VarInsnNode(ASTORE, methodType.beanVar));
        // store string hashcode in local variable
        inst.add(new VarInsnNode(ALOAD, methodType.nameVar));
        inst.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "hashCode", "()I"));
        inst.add(new VarInsnNode(ISTORE, methodType.hashVar));
        process(inst, type, properties, 0, properties.size(), methodType, interfaze ? INVOKEINTERFACE : INVOKEVIRTUAL);
    }

    /**
     * Adds the access block for the specified property.
     * 
     * @param entry The entry describing the property
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the method
     * @param methodType The type of the method being built
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     * @param forcedCheck True if the equals check of the name is necessary (so the hashcode exists at least two times)
     */
    private static void addPropertyBlock(SimpleEntry entry, String type, InsnList inst, AccessType methodType, int invokeOpcode, boolean forcedCheck) {
        LabelNode l = null;
        // name check
        if (forcedCheck)
            addCheckStringBlock(entry, inst, methodType, l = new LabelNode());
        // getter
        if (AccessType.GETTER == methodType) {
            if (entry.method.getReturnType().isPrimitive())
                addPrimitiveGetterBlock(entry, type, inst, invokeOpcode);
            else
                addObjectGetterBlock(entry, type, inst, invokeOpcode);
        } else {
            // setter
            if (entry.method.getParameterTypes()[0].isPrimitive())
                addPrimitiveSetterBlock(entry, type, inst, invokeOpcode);
            else
                addObjectSetterBlock(entry, type, inst, invokeOpcode);
        }
        // block end (only needed for forced checks)
        if (forcedCheck)
            inst.add(l);
    }

    /**
     * Adds the name check for the specified property.
     * 
     * @param entry The entry describing the property
     * @param inst The instruction list of the method
     * @param methodType The type of the method being built
     * @param l The label of the block end
     */
    private static void addCheckStringBlock(SimpleEntry entry, InsnList inst, AccessType methodType, final LabelNode l) {
        // check string
        inst.add(new LdcInsnNode(entry.name));
        inst.add(new VarInsnNode(ALOAD, methodType.nameVar));
        inst.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z"));
        inst.add(new JumpInsnNode(IFEQ, l));
    }

    /**
     * Adds the setter instructions for the specified object property.
     * 
     * @param entry The entry describing the property
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the {@link IPropertyAccessor#get(Object, String)} method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void addObjectSetterBlock(SimpleEntry entry, String type, InsnList inst, int invokeOpcode) {
        final Method m = entry.method;
        inst.add(new VarInsnNode(ALOAD, AccessType.SETTER.beanVar));
        inst.add(new VarInsnNode(ALOAD, SETTER_VAL));
        inst.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(m.getParameterTypes()[0])));
        inst.add(new MethodInsnNode(invokeOpcode, type, m.getName(), Type.getMethodDescriptor(m)));
        inst.add(new InsnNode(RETURN));
    }

    /**
     * Adds the setter instructions for the specified primitive property.
     * 
     * @param entry The entry describing the property
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the {@link IPropertyAccessor#get(Object, String)} method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void addPrimitiveSetterBlock(SimpleEntry entry, String type, InsnList inst, int invokeOpcode) {
        final Method m = entry.method;
        final PrimitiveType wrapper = PrimitiveType.get(m.getParameterTypes()[0]);
        inst.add(new VarInsnNode(ALOAD, AccessType.SETTER.beanVar));
        inst.add(new VarInsnNode(ALOAD, SETTER_VAL));
        inst.add(new TypeInsnNode(CHECKCAST, wrapper.wrapper));
        inst.add(new MethodInsnNode(INVOKEVIRTUAL, wrapper.wrapper, wrapper.unwrapMethod, wrapper.unwrapDescr));
        inst.add(new MethodInsnNode(invokeOpcode, type, m.getName(), Type.getMethodDescriptor(m)));
        inst.add(new InsnNode(RETURN));
    }

    /**
     * Adds the getter instructions for the specified primitive property.
     * 
     * @param entry The entry describing the property
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the {@link IPropertyAccessor#get(Object, String)} method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void addPrimitiveGetterBlock(SimpleEntry entry, String type, InsnList inst, int invokeOpcode) {
        final Method m = entry.method;
        final PrimitiveType wrapper = PrimitiveType.get(m.getReturnType());
        inst.add(new VarInsnNode(ALOAD, AccessType.GETTER.beanVar));
        inst.add(new MethodInsnNode(invokeOpcode, type, m.getName(), Type.getMethodDescriptor(m)));
        inst.add(new MethodInsnNode(INVOKESTATIC, wrapper.wrapper, PrimitiveType.WRAP_METHOD, wrapper.wrapDescr));
        inst.add(new InsnNode(ARETURN));
    }

    /**
     * Adds the getter instructions for the specified object property.
     * 
     * @param entry The entry describing the property
     * @param type The internal name of the type for which the accessor is built
     * @param inst The instruction list of the {@link IPropertyAccessor#get(Object, String)} method
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void addObjectGetterBlock(SimpleEntry entry, String type, InsnList inst, int invokeOpcode) {
        final Method m = entry.method;
        inst.add(new VarInsnNode(ALOAD, AccessType.GETTER.beanVar));
        inst.add(new MethodInsnNode(invokeOpcode, type, m.getName(), Type.getMethodDescriptor(m)));
        inst.add(new InsnNode(ARETURN));
    }

    /**
     * Recursive method used to build a binary search like implementation for the property access.
     * 
     * @param inst The instruction list of the {@link IPropertyAccessor#get(Object, String)} method
     * @param type The internal name of the type for which the accessor is built
     * @param properties The properties of the type
     * @param start The start index inside the properties list (inclusive)
     * @param end The end index inside the properties list (exclusive)
     * @param methodType The type of the method being built
     * @param invokeOpcode The opcode to use for the getter/setter invocations {@link Opcodes#INVOKEINTERFACE} or {@link Opcodes#INVOKEVIRTUAL}
     */
    private static void process(InsnList inst, String type, List<HashEntry> properties, int start, int end, AccessType methodType, int invokeOpcode) {
        int delta = (end - start);
        if (delta == 0)
            throw new IllegalStateException("Found 0 delta at " + start);
        if (delta == 1) {
            final HashEntry entry = properties.get(start);
            final int cnt = entry.getEntryCount();
            if (cnt == 1)
                addPropertyBlock(entry.getEntry(0), type, inst, methodType, invokeOpcode, false);
            else
                for (int i = 0; i < cnt; i++)
                    addPropertyBlock(entry.getEntry(i), type, inst, methodType, invokeOpcode, true);
            return;
        }
        final int mid = start + delta / 2;
        LabelNode l = new LabelNode();
        inst.add(new VarInsnNode(ILOAD, methodType.hashVar));
        inst.add(new LdcInsnNode(new Integer(properties.get(mid).hash)));
        inst.add(new JumpInsnNode(IF_ICMPGE, l));
        // upper part
        process(inst, type, properties, start, mid, methodType, invokeOpcode);
        // else block
        inst.add(l);
        // lower part
        process(inst, type, properties, mid, end, methodType, invokeOpcode);
    }

    /**
     * Returns the properties of the given class.
     * 
     * @param c The class
     * @param getters List to populate with getter {@link HashEntry}s, will be sorted by hashcode
     * @param setters List to populate with setter {@link HashEntry}s, will be sorted by hashcode
     * @throws IllegalArgumentException If the class was not introspectable
     */
    private static void buildPropertyLists(Class<?> c, List<HashEntry> getters, List<HashEntry> setters) throws IllegalArgumentException {
        try {
            for (PropertyDescriptor p : Introspector.getBeanInfo(c).getPropertyDescriptors()) {
                // skip indexed properties
                if (p instanceof IndexedPropertyDescriptor)
                    continue;
                final String name = p.getName();
                final Method read = p.getReadMethod();
                if (read != null)
                    getters.add(new SimpleEntry(name, read));
                final Method write = p.getWriteMethod();
                if (write != null)
                    setters.add(new SimpleEntry(name, write));
            }
            Collections.sort(getters);
            Collections.sort(setters);
            // flag duplicated hashes
            buildGroups(getters);
            buildGroups(setters);
        } catch (IntrospectionException e) {
            throw new IllegalArgumentException(c.getName() + " is not 'introspectable'!", e);
        }
    }

    /**
     * Builds {@link EntryGroup}s wherever necessary.
     * 
     * @param properties The properties to process
     */
    private static void buildGroups(List<HashEntry> properties) {
        int hash = 0;
        for (int i = properties.size() - 1; i > -1; i--) {
            HashEntry e = properties.get(i);
            if (e.hash == hash) {
                // the number of entries with the same hashcode
                int cnt = 1;
                // search the last entry with the same hashcode
                for (; i > -1 && (e = properties.get(i)).hash == hash; i--)
                    cnt++;
                buildGroup(i + 1, cnt, properties);
            }
            hash = e.hash;
        }
    }

    /**
     * Builds a {@link EntryGroup} out of the specified elements.<br>
     * The elements will be removed and a {@link EntryGroup} will be inserted at the start position.
     * 
     * @param start The start index
     * @param cnt The number of elements
     * @param properties The elements
     */
    private static void buildGroup(int start, int cnt, List<HashEntry> properties) {
        final SimpleEntry[] entries = new SimpleEntry[cnt];
        for (int i = 0; i < entries.length; i++)
            entries[i] = properties.remove(start).getEntry(0);
        properties.add(start, new EntryGroup(entries[0].hash, entries));
    }

    /**
     * Reset the internal cache.<br>
     * Consider to use this method only for testing purposes.
     */
    public static void clearCache() {
        INSTANCES.clear();
    }
}
